package edu.psu.ist131.lib;

import java.io.*;
import java.security.MessageDigest;
import java.util.*;
//import java.util.logging.Level;
//import java.util.logging.Logger;
import javax.persistence.Id;
import javax.swing.table.DefaultTableModel;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeModel;

public class FileMetadata
{

    @Id
    private Object ID;                  // for use by OrientDB ONLY!!!
    private String absolutePath;
    private String name;
    private String extension;
    private String checksum;
    private Date lastModified;
    private long size;
    private boolean isHidden;
    private boolean isExecutable;
    private boolean isReadable;
    private boolean isWriteable;


    public long getSize()
    {
        return size;
    }


    public String getAbsolutePath()
    {
        return absolutePath;
    }


    public String getExtension()
    {
        return extension;
    }


    public String getChecksum()
    {
        return checksum;
    }


    public Date getLastModified()
    {
        return lastModified;
    }


    public String getName()
    {
        return name;
    }


    public boolean isIsExecutable()
    {
        return isExecutable;
    }


    public boolean isIsHidden()
    {
        return isHidden;
    }


    public boolean isIsReadable()
    {
        return isReadable;
    }


    public boolean isIsWriteable()
    {
        return isWriteable;
    }

    /***
     * 
     * @param filepath  - the file to analyze
     * @throws Exception 
     */
    public FileMetadata(String filepath) throws Exception
    {
        File f = new File(filepath);
        if(!f.isFile())
        {
            throw new Exception("'" + filepath + "' is not the path to a valid file");
        }
        name = f.getName();
        absolutePath = f.getAbsolutePath();
        checksum = getMD5Checksum(absolutePath);
        absolutePath = absolutePath.substring(0, absolutePath.length() - name.length());
        extension = "";
        String[] foo = name.split("\\.");
        if(foo.length > 1)
        {
            extension = foo[foo.length - 1];
        }
        lastModified = new Date(f.lastModified());
        size = f.length();
        isHidden = f.isHidden();
        isExecutable = f.canExecute();
        isReadable = f.canRead();
        isWriteable = f.canWrite();
    }


    /***
     * 
     * @param directoryPath - the base directory to search for files
     * @param filter - the implementation of FilenameFilter to use (or 'null' for all files)
     * @param recurse - if false, only the initial directory is searched; otherwise, all directories within the base will be searched as well
     * @return 
     */
    public static Collection<FileMetadata> getFiles(String directoryPath, FilenameFilter filter, boolean recurse)
    {
        File directory = new File(directoryPath);
        return getFiles(directory, filter, recurse);
    }


    /***
     * 
     * @param files - a collection of FileMetadata items to sort through
     * @param fields - a list of the fields to include when determining uniqueness
     * @param excludeNonDuplicates - if true, the list will only include "collisions"
     * @return 
     */
    public static HashMap<String, Collection<FileMetadata>> getPossibleDuplicates(Collection<FileMetadata> files, Collection<String> fields, boolean excludeNonDuplicates)
    {
        HashMap<String, Collection<FileMetadata>> dupes = new HashMap<String, Collection<FileMetadata>>();
        for(FileMetadata file : files)
        {
            String hash = generateValueListFromFieldList(file, fields);
            if(!dupes.containsKey(hash))
            {
                dupes.put(hash, new ArrayList<FileMetadata>());
            }
            dupes.get(hash).add(file);
        }
        if(excludeNonDuplicates)
        {
            HashMap<String, Collection<FileMetadata>> onlyDupes = new HashMap<String, Collection<FileMetadata>>();
            for(String key : dupes.keySet())
            {
                Collection<FileMetadata> a = dupes.get(key);
                if(a.size() > 1)
                {
                    onlyDupes.put(key, a);
                }
            }
            dupes = onlyDupes;
        }
        return dupes;
    }

    
    private static String generateValueListFromFieldList(FileMetadata file, Collection<String> fields)
    {
        StringBuilder retval = new StringBuilder("`");
        Collection<String> values = generateCollectionFromFieldList(file, fields);
        for(String val : values)
        {
            retval.append(val);
            retval.append("`");
        }
        return retval.toString();
    }
    
    /**
     * 
     * @param file
     * @param fields
     * @return 
     */
    private static Collection<String> generateCollectionFromFieldList(FileMetadata file, Collection<String> fields)
    {
        ArrayList<String> retval = new ArrayList<String>();
        for(String field : fields)
        {
            retval.add(getValueByName(file, field));
        }
        return retval;
    }
    
    
    public static String getValueByName(FileMetadata file, String fieldname)
    {
        String retval = "";
        String fld = fieldname.toLowerCase();
        if(fld.contains("path"))
        {
            retval = file.getAbsolutePath();
        }
        else if(fld.contains("hash"))
        {
            retval = file.getChecksum();
        }
        else if(fld.contains("ext"))
        {
            retval = file.getExtension();
        }
        else if(fld.contains("mod"))
        {
            retval = file.getLastModified().toString();
        }
        else if(fld.contains("name"))
        {
            retval = file.getName();
        }
        else if(fld.contains("size"))
        {
            retval = (new Long(file.getSize())).toString();
        }
        return retval;
    }
    


    /***
     * 
     * @param directoryPath - the base directory to search for files
     * @param filter - the implementation of FilenameFilter to use (or 'null' for all files)
     * @param recurse - if false, only the initial directory is searched; otherwise, all directories within the base will be searched as well
     * @return 
     */
    public static Collection<FileMetadata> getFiles(File directory, FilenameFilter filter, boolean recurse)
    {
        Vector<FileMetadata> retval = new Vector<FileMetadata>();
        Collection<File> files = listFiles(directory, filter, recurse);
        for(File file : files)
        {
            if(file.isFile())
            {
                try
                {
                    retval.add(new FileMetadata(file.getAbsolutePath()));
                }
                catch(Exception ex)
                {
                    // won't happen -- we already checked for the exceptional condition                    //Logger.getLogger(FileMetadata.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        }
        return retval;
    }
    
    
    public static DefaultTableModel getTableModel(Collection<FileMetadata> files, Collection<String> fields)
    {
        Vector<String> tblHeaders = new Vector<String>();
        for(String f : fields)
        {
            tblHeaders.add(f);
        }

        Vector<Vector<String>> tblData = new Vector<Vector<String>>();
        for(FileMetadata file : files)
        {
            Vector<String> row = new Vector<String>();
            Collection<String> values = generateCollectionFromFieldList(file, fields);
            for(String val : values)
            {
                row.add(val);
            }
            tblData.add(row);
        }
        return new DefaultTableModel(tblData, tblHeaders);
    }    
    
    
    public static TreeModel getTreeModelOfDuplicates(HashMap<String, Collection<FileMetadata>> dupes)
    {
        DefaultMutableTreeNode root = new DefaultMutableTreeNode();
        for(String key : dupes.keySet())
        {
            Collection<FileMetadata> dupeset = dupes.get(key);
            String nodename = new Integer(dupeset.size()).toString() + " in " + key;
            DefaultMutableTreeNode dupegroup = new DefaultMutableTreeNode(nodename);
            root.add(dupegroup);
            for(FileMetadata file : dupeset)
            {
                DefaultMutableTreeNode item = new DefaultMutableTreeNode(file.getName());
                dupegroup.add(item);
            }
        }
        return new DefaultTreeModel(root);
    }
    

    public static TreeModel getTreeModelOfDirectories(String directoryPath, FilenameFilter filter, boolean recurse, boolean excludeEmptyDirs)
    {
        Collection<FileMetadata> allFiles = getFiles(directoryPath, filter, recurse);
        ArrayList<String> fieldnames = new ArrayList<String>();       
        fieldnames.add("path");
        HashMap<String, Collection<FileMetadata>> dirCollection = getPossibleDuplicates(allFiles, fieldnames, false);
        
        DefaultMutableTreeNode root = new DefaultMutableTreeNode( new Integer(allFiles.size()).toString() + " files in " +  directoryPath);
        for(String key : dirCollection.keySet())
        {
            Collection<FileMetadata> dirset = dirCollection.get(key);
            if(excludeEmptyDirs && dirset.size() < 1)
            {
                continue;
            }
            String nodename = new Integer(dirset.size()).toString() + " files in " + key;
            DefaultMutableTreeNode dupegroup = new DefaultMutableTreeNode(nodename);
            root.add(dupegroup);
            for(FileMetadata file : dirset)
            {
                DefaultMutableTreeNode item = new DefaultMutableTreeNode(file.getName());
                dupegroup.add(item);
            }
        }
        return new DefaultTreeModel(root);
    }
    

    public static Collection<File> listFiles(File directory, FilenameFilter filter, boolean recurse)
    {
        Vector<File> files = new Vector<File>();    // List of files / directories
        File[] entries = directory.listFiles();     // Get files / directories in the directory

        for(File entry : entries)                   // Go over entries
        {
            // If there is no filter or the filter accepts the  file / directory, add it to the list
            if(filter == null || filter.accept(directory, entry.getName()))
            {
                files.add(entry);
            }

            if(recurse && entry.isDirectory())
            {
                files.addAll(listFiles(entry, filter, recurse));    // recurse into the directory
            }
        }
        return files;                               // Return collection of files
    }


    // MD5 code from http://stackoverflow.com/questions/304268/getting-a-files-md5-checksum-in-java
    private static byte[] createChecksum(String filename) throws Exception
    {
        InputStream fis = new FileInputStream(filename);
        byte[] buffer = new byte[1024];
        MessageDigest complete = MessageDigest.getInstance("MD5");
        int numRead;
        do
        {
            numRead = fis.read(buffer);
            if(numRead > 0)
            {
                complete.update(buffer, 0, numRead);
            }
        } while(numRead != -1);
        fis.close();
        return complete.digest();
    }


    private static String getMD5Checksum(String filename) throws Exception
    {
        byte[] b = createChecksum(filename);
        String result = "";
        for(int i = 0; i < b.length; i++)
        {
            result += Integer.toString((b[i] & 0xff) + 0x100, 16).substring(1);
        }
        return result;
    }


    public String ToString()
    {
        StringBuffer sb = new StringBuffer();
        sb.append("file: " + getAbsolutePath() + getName());
        if(ID != null)
        {
            sb.append('\n' + "ID: " + ID.toString());
        }
        return sb.toString();
    }


}
